Unify reflection helpers#7344
Conversation
# Conflicts: # src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs # test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestPropertyAttributeTests.cs # test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TypeCacheTests.cs # test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/ReflectHelperTests.cs
…tion # Conflicts: # src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs # src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs # src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentItemUtility.cs # src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs # src/Adapter/MSTestAdapter.PlatformServices/Utilities/ReflectionOperationsNetFrameworkAttributeHelpers.cs # test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/ReflectionUtilityTests.cs # test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestDeploymentTests.cs # test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestDeploymentTests.cs # test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentItemUtilityTests.cs # test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentUtilityTests.cs # test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/ReflectionUtilityTests.cs
Youssef1313
left a comment
There was a problem hiding this comment.
I guess we are good now that everything uses the caching ReflectHelper on current main?
(Except that we need to investigate if it's possible to simplify the implementation of reflection operations under .NET Framework)
|
@copilot resolve the merge conflicts in this pull request |
…tion # Conflicts: # src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumerator.cs # src/Adapter/MSTestAdapter.PlatformServices/Discovery/TypeEnumerator.cs # src/Adapter/MSTestAdapter.PlatformServices/Execution/TypeCache.cs # src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectHelper.cs # test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TestMethodValidatorTests.cs # test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeEnumeratorTests.cs # test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TypeCacheTests.cs # test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/UnitTestRunnerTests.cs Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Resolved the merge conflicts and pushed the merge commit: Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot fix the following issues: @azure-pipelines src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.cs(68,36): error CS1503: (NETCORE_ENGINEERING_TELEMETRY=Build) Argument 1: cannot convert from 'Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.IReflectionOperations' to 'Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers.ReflectHelper' @azure-pipelines src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumerator.cs(146,47): error CS1503: (NETCORE_ENGINEERING_TELEMETRY=Build) Argument 1: cannot convert from 'Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers.ReflectHelper' to 'Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.IReflectionOperations' |
Fixed and pushed in I aligned the affected reflection-related files to consistent types, which resolves the CS1503 constructor/signature mismatches and related compile errors. UI screenshot: N/A (no UI changes in this PR). Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
# Conflicts: # src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs
There was a problem hiding this comment.
Pull request overview
This PR addresses #3043 by consolidating reflection-related behavior around IReflectionOperations/ReflectionOperations, replacing several usages of ReflectHelper in PlatformServices (and updating tests accordingly) while introducing a small ReflectionHelper utility for test metadata extraction.
Changes:
- Extend
IReflectionOperationsandReflectionOperationswith cached/generic attribute APIs (e.g.,GetAttributes<T>,GetCustomAttributesCached,IsAttributeDefined<T>). - Update deployment + execution code paths to use
IReflectionOperationsinstead ofReflectHelperin key locations. - Refactor and expand unit/integration tests, adding
MockableReflectionOperationsto support Moq scenarios.
Show a summary per file
| File | Description |
|---|---|
| test/UnitTests/MSTestAdapter.UnitTests/MSTestAdapter.UnitTests.csproj | Links shared test helper MockableReflectionOperations into MSTestAdapter.UnitTests. |
| test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/ReflectionUtilityTests.cs | Updates tests to use ReflectionOperations instance and adds generic attribute tests. |
| test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentUtilityTests.cs | Switches deployment tests to mock IReflectionOperations instead of ReflectHelper. |
| test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentItemUtilityTests.cs | Switches to IReflectionOperations mocks and adjusts helper signatures. |
| test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs | Wraps mocked reflection ops with MockableReflectionOperations to support generic API usage. |
| test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/MockableReflectionOperations.cs | New test-only wrapper to make IReflectionOperations easier to use with Moq. |
| test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestDeploymentTests.cs | Updates to use IReflectionOperations in deployment tests. |
| test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/ReflectionOperationsTests.cs | Moves/rewrites tests previously covering ReflectHelper onto ReflectionOperations. |
| test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestDeploymentTests.cs | Uses IReflectionOperations for deployment item utility in desktop tests. |
| test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopReflectionOperationsTests.cs | Adjusts assertions to account for compiler-generated attributes. |
| test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/ReflectHelperTests.cs | Removes obsolete ReflectHelper test suite (migrated to ReflectionOperationsTests). |
| test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Extensions/MethodInfoExtensionsTests.cs | Updates extension method tests to pass IReflectionOperations. |
| test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestPropertyAttributeTests.cs | Switches cache clearing + trait extraction to ReflectionOperations/TypeCache. |
| test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodRunnerTests.cs | Updates reflection-related setups to new IReflectionOperations API shape. |
| test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/ClassCleanupManagerTests.cs | Removes unused ReflectHelper usage (but now has an unused using). |
| test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeValidatorTests.cs | Attempts to swap ReflectHelper mocks for IReflectionOperations mocks. |
| test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/AssemblyEnumeratorTests.cs | Attempts to construct discovery components using IReflectionOperations wrappers. |
| test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/ReflectionUtilityTests.cs | Switches from reflection-only loading to Assembly.LoadFrom and adds generic attribute coverage. |
| src/Adapter/MSTestAdapter.PlatformServices/Utilities/ReflectionOperationsNetFrameworkAttributeHelpers.cs | Removes NETFRAMEWORK reflection-only attribute helper implementation. |
| src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs | Updates default ctor to use ReflectionOperations from PlatformServiceProvider. |
| src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentItemUtility.cs | Changes dependency from ReflectHelper to IReflectionOperations. |
| src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs | Updates default ctor to use ReflectionOperations from PlatformServiceProvider. |
| src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs | Adds caching + generic attribute APIs and removes NETFRAMEWORK special casing. |
| src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IReflectionOperations.cs | Expands interface with generic/cached attribute APIs. |
| src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectionHelper.cs | Introduces helper extensions for categories and test property traits. |
| src/Adapter/MSTestAdapter.PlatformServices/Helpers/AttributeHelpers.cs | Routes ignore-condition logic via PlatformServiceProvider.Instance.ReflectionOperations. |
| src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs | Uses ReflectionOperations.GetCustomAttributesCached via platform service provider. |
| src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.cs | Routes attribute queries via PlatformServiceProvider.Instance.ReflectionOperations. |
| src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblySettingsProvider.cs | Replaces static ReflectHelper assembly attribute checks with IReflectionOperations. |
Copilot's findings
Comments suppressed due to low confidence (2)
src/Adapter/MSTestAdapter.PlatformServices/Services/ReflectionOperations.cs:277
- GetCustomAttributesNotCached assigns the result of GetCustomAttributes(MemberInfo) (nullable) into a non-nullable object[] local. This produces nullable warnings and can hide an actual null return. Make the local object[]? (or coalesce to Array.Empty()) so the nullability matches the method’s contract.
public static object[]? GetCustomAttributesNotCached(ICustomAttributeProvider attributeProvider) { IReflectionOperations reflectionOperations = PlatformServiceProvider.Instance.ReflectionOperations; object[] attributesArray = attributeProvider is MemberInfo memberInfo ? reflectionOperations.GetCustomAttributes(memberInfo) : reflectionOperations.GetCustomAttributes((Assembly)attributeProvider, typeof(Attribute));test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeValidatorTests.cs:107
- Same constructor mismatch here: TypeValidator currently expects a ReflectHelper (and overload with discoverInternals also takes ReflectHelper), but this test passes IReflectionOperations. This should be adjusted to match the production signature (or update production code to use IReflectionOperations).
public void WhenInternalDiscoveryIsEnabledIsValidTestClassShouldReturnTrueForInternalTestClasses() { var typeValidator = new TypeValidator(_mockReflectionOperations.Object, true);- Files reviewed: 29/29 changed files
- Comments generated: 10
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Test Expert Reviewer 🧪
Date: 2026-05-11
Repository: microsoft/testfx
PR: #7344 — Unify reflection helpers
Key Findings
This PR refactors tests to use the new unified ReflectionOperations/IReflectionOperations API (replacing ReflectHelper). The migration is structurally sound — the deleted ReflectHelperTests.cs tests have been faithfully ported into ReflectionOperationsTests.cs, and the new MockableReflectionOperations helper is well-designed. Tests correctly manage PlatformServiceProvider.Instance state via constructor setup and Dispose.
The following issues are all non-blocking cleanup items introduced during the migration:
-
[Assertion] 12 occurrences of
SequenceEqual(...).Should().BeTrue()— These produce completely opaque failure messages on CI. Useactual.Should().Equal(expected)for sequence-order assertions (shown in inline comment at line 59 ofReflectionOperationsTests.cs). -
[Structure] Stale test names
HasAttributeDerivedFrom*— These methods now callrh.IsAttributeDefined<T>()internally (the oldHasAttributeDerivedFrommethod no longer exists). Names should be updated to reflect what's actually being tested. -
[Structure] Two dead code comments —
// new Mock<MemberInfo>();at lines 258 and 305 should be removed. -
[Assertion] Redundant Moq setup in
TypeValidatorTests— Two identicalIt.IsAny<TypeInfo>()setups where the first (Returns(false)) is immediately overridden by the second (Returns(true)). Caused by a matcher change during migration fromIt.IsAny<Type>().
Recommendations
All issues are minor clean-up items that don't affect test correctness or reliability. No flakiness risks or false-positives were identified. The PR can proceed — the suggestions can be addressed in a follow-up or in this PR at the author's discretion.
Generated by Test Expert Reviewer
🧪 Test quality reviewed by Test Expert Reviewer 🧪
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: pr-expert-reviewer 🧠
Date: 2026-05-11
Repository: microsoft/testfx
Key Findings
🔴 Correctness — Unsafe concrete-type casts (2 occurrences)
Both TestDeployment.cs:37 and DeploymentUtilityBase.cs:28 were changed to use the new IReflectionOperations abstraction — but both no-arg constructors still cast through to the concrete type:
new DeploymentItemUtility((ReflectionOperations)PlatformServiceProvider.Instance.ReflectionOperations)DeploymentItemUtility now accepts IReflectionOperations, making the cast entirely unnecessary. It will throw InvalidCastException any time the active platform-service provider returns something other than the concrete ReflectionOperations (e.g., test scenarios using SetupMockReflectionOperations() / MockableReflectionOperations).
🟡 Correctness — DeclaringType! null-forgiving on a nullable property
IsMethodDeclaredInSameAssemblyAsType uses method.DeclaringType!.Assembly — the null-forgiving operator suppresses the compiler warning but does not prevent a NullReferenceException if a MethodInfo with DeclaringType == null is ever passed. A ?. guard with an appropriate fallback is the safer pattern.
Positive Observations
- The overall design is good: moving attribute caching directly into
ReflectionOperations(away fromReflectHelper) is the right architectural move and significantly reduces duplication. ConcurrentDictionary<ICustomAttributeProvider, Attribute[]>for the attribute cache is thread-safe and well-suited for the parallel test-execution model.MockableReflectionOperationselegantly bridges the Moq generic-method-constraint gap and enables clean test isolation.- The
GetTestPropertiesAsTraitsrefactor (two-pass count + single allocation) is a nice performance improvement. - Deletion of
ReflectionOperationsNetFrameworkAttributeHelpers.cs(reflection-only load path) appears safe: code that still needs reflection-only load (AssemblyLoadWorker) already accessesCustomAttributeData.GetCustomAttributesdirectly and does not go throughReflectionOperations.
Generated by pr-expert-reviewer 🧠
🧠 Reviewed by Expert Code Reviewer 🧠
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Test Expert Reviewer 🧪
Date: 2026-05-11
Repository: microsoft/testfx
PR: #7344 — Unify reflection helpers (updated review, head 0253483)
Key Findings
This review covers the new commits since the previous automated review (52b4576 → 0253483). The migration from ReflectHelper to IReflectionOperations across the test suite is structurally sound. MockableReflectionOperations is well-designed and correctly bridges the Moq generic-constraint gap. TestablePlatformServiceProvider.SetupMockReflectionOperations wrapping is correct. The new GetTestCategories and IsAttributeDefined tests are well-isolated (state cleaned up via Dispose).
Four issues found — none are flakiness/isolation bugs, all are quality improvements:
-
[Structure] Dead
SetupDeploymentItemsmethod with pragma suppression inDesktopTestDeploymentTests.cs— the method is never called; a validIDE0051warning is suppressed rather than the dead code being removed or wired in. -
[Assertion]
AttributeMockingHelper.SetCustomAttributeignores itsTypeparameter — the storedTypeis never used inGetCustomAttributesNotCached, making the parameter misleading and creating a latent false-positive risk for future tests. -
[Assertion]
BeEquivalentToused for ordered-sequence assertions inReflectionUtilityTests.cs(lines 49, 63, 129) — where the test clearly expects a specific attribute order (derived before base), order-insensitive comparison masks regressions. -
[Assertion]
SequenceEqual(...).Should().BeTrue()(12 occurrences) — already flagged in the prior review; still unaddressed. Produces opaque CI failures.
Recommendations
- Remove or wire in
SetupDeploymentItemsinDesktopTestDeploymentTests.cs - Either apply type-based filtering in
GetCustomAttributesNotCached, or drop theType typeparameter fromSetCustomAttribute - Replace
BeEquivalentTowithEqualfor tests that encode a meaningful attribute order - Replace
SequenceEqual(...).Should().BeTrue()withactual.Should().Equal(expected)throughout
Generated by Test Expert Reviewer 🧪
🧪 Test quality reviewed by Test Expert Reviewer 🧪
🧪 Test quality reviewed by Test Expert Reviewer 🧪
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Expert Code Reviewer 🧠
Date: 2026-05-11
Repository: microsoft/testfx
Key Findings
🔴 Correctness — Two surviving concrete-type casts (unresolved from prior review)
TestDeployment.cs:37 and DeploymentUtilityBase.cs:28 both no-arg constructors still contain (ReflectionOperations)PlatformServiceProvider.Instance.ReflectionOperations. Since DeploymentItemUtility was updated to accept IReflectionOperations, the cast is entirely unnecessary — and actively harmful. It will throw InvalidCastException at runtime when the service provider resolves to anything other than the concrete ReflectionOperations (e.g., the MockableReflectionOperations now in the test project). The "Fix build errors" commit (latest, 0253483) did not address these.
🔴 Correctness — DeclaringType! null-forgiving in IsMethodDeclaredInSameAssemblyAsType (unresolved from prior review)
method.DeclaringType!.Assembly.Equals(type.Assembly) at line 287 of ReflectionOperations.cs silences the compiler but does not prevent a NullReferenceException. MethodInfo.DeclaringType is nullable by contract (returns null for global IL methods). A ?. guard returning false is the correct fix.
🟡 Correctness — Unguarded (Assembly) cast in NotCachedReflectionAccessor (new)
NotCachedReflectionAccessor.GetCustomAttributesNotCached (line 279) falls back to (Assembly)attributeProvider when the provider is not a MemberInfo. ICustomAttributeProvider is also implemented by Module and ParameterInfo — passing either silently throws InvalidCastException. A switch expression covering all known cases with a descriptive exception for the rest would prevent silent surprises.
Positive Observations
- Moving the attribute cache from the old
ReflectHelperstatic singleton intoReflectionOperationsis the right structural improvement;ConcurrentDictionary<ICustomAttributeProvider, Attribute[]>is thread-safe and handles parallel test execution correctly. - The
NotCachedReflectionAccessorinner class cleanly separates cached and uncached access paths, and thestatic Attribute[] GetAttributes(...)local function avoids aFunc<>allocation on every cache-miss. GetTestPropertiesAsTraits's two-pass count + single allocation pattern is a genuine performance win.ClearCache()is correctly scoped to test manipulation scenarios and doesn't touch shared state.
🧠 Reviewed by Expert Code Reviewer 🧠
🧠 Reviewed by Expert Code Reviewer 🧠
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Expert Code Reviewer 🧠
Date: 2026-05-11
Repository: microsoft/testfx
Key Findings
All three issues flagged in prior reviews have been correctly resolved in commit fae08031:
✅ Resolved — Unsafe (ReflectionOperations) casts removed
Both TestDeployment and DeploymentUtilityBase no-arg constructors now pass PlatformServiceProvider.Instance.ReflectionOperations directly to DeploymentItemUtility without a concrete-type cast. The InvalidCastException risk when using MockableReflectionOperations in tests is eliminated.
✅ Resolved — DeclaringType! null-forgiving operator replaced
IsMethodDeclaredInSameAssemblyAsType now correctly uses method.DeclaringType?.Assembly.Equals(type.Assembly) ?? false, guarding against global IL methods where DeclaringType is null by contract.
✅ Resolved — NotCachedReflectionAccessor hard-cast replaced
The (Assembly)attributeProvider cast is replaced with a well-structured switch expression that explicitly handles MemberInfo and Assembly cases, and throws a descriptive ArgumentException (which is caught and logged upstream) for unsupported providers like Module or ParameterInfo.
Positive Observations
- The switch expression in
GetCustomAttributesNotCachedis clean and correctly handles all framework-significant cases; the_ => throw new ArgumentException(...)arm produces a descriptive diagnostic rather than a silent miscast. - The
?.null-conditional onDeclaringTypewith a?? falsefallback is exactly the right pattern. - No new correctness, threading, or API-compatibility concerns were introduced in this commit.
🧠 Reviewed by Expert Code Reviewer 🧠
🧠 Reviewed by Expert Code Reviewer 🧠
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Test Expert Reviewer 🧪
Date: 2026-05-11
Repository: microsoft/testfx
PR: #7344 — Unify reflection helpers (updated review, head fae0803)
What Was Fixed in This Commit ✅
This commit ("Address review comments") resolves the four issues flagged in the prior review:
- All 12
SequenceEqual(...).Should().BeTrue()occurrences inReflectionOperationsTests.cs→ replaced withactual.Should().Equal(expected)✅ - 3×
BeEquivalentToinReflectionUtilityTests.cs(integration) → replaced withEqual✅ - Dead
SetupDeploymentItemsmethod inDesktopTestDeploymentTests.cs→ removed ✅ MockableReflectionOperations.GetCustomAttributesCached→ null-safe for the Assembly branch ✅AssemblyEnumeratorTests.TestableAssemblyEnumerator→ simplified toMock<ReflectHelper>directly ✅
Remaining Issues
Two items from the earlier reviews are still open:
-
[Assertion]
ReflectionUtilityTests.cs:30— OneSequenceEqual(...).Should().BeTrue()was missed because it lives inside a#if NETFRAMEWORKblock. (Inline comment posted.) -
[Assertion]
AttributeMockingHelper.SetCustomAttribute— theType typeparameter is stored but never used during filtering inGetCustomAttributesNotCached. The actual type filtering happens upstream via.OfType<T>()inMockableReflectionOperations, so there's no false-positive today, but the API is misleading. (Inline comment posted.)
Two items from the first review (run 25663966959) remain open but are minor/style-level:
- Stale test method names
HasAttributeDerivedFrom*(lines 272, 285, 298, 319 ofReflectionOperationsTests.cs) — these methods now callIsAttributeDefined<T>internally. - Dead code comments
// new Mock<MemberInfo>();(lines 258, 305 ofReflectionOperationsTests.cs).
Overall Assessment
No flakiness or isolation bugs. The migration from ReflectHelper to IReflectionOperations is sound and well-tested. All remaining items are minor clean-up suggestions.
🧪 Test quality reviewed by Test Expert Reviewer 🧪
🧪 Test quality reviewed by Test Expert Reviewer 🧪
|
@copilot resolve the merge conflicts in this pull request |
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Merge conflicts are resolved in commit |
There was a problem hiding this comment.
Copilot's findings
Comments suppressed due to low confidence (4)
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodRunnerTests.cs:255
- This GetAttributes(...) setup targets the Moq mock, but TestablePlatformServiceProvider exposes a MockableReflectionOperations wrapper whose generic methods filter GetCustomAttributes results instead of delegating to the mock's generic setups. As a result this setup is unused and can be misleading; prefer ensuring GetCustomAttributes(methodInfo) returns the DataSourceAttribute you want the wrapper to surface.
// Setup mocks
_testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetCustomAttributes(methodInfo)).Returns(attributes);
_testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetAttributes<DataSourceAttribute>(methodInfo)).Returns(attributes.OfType<DataSourceAttribute>());
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodRunnerTests.cs:283
- Same as above: setting up MockReflectionOperations.GetAttributes(...) has no effect when calls go through MockableReflectionOperations (it filters GetCustomAttributes instead). Consider removing this setup and relying on GetCustomAttributes(_methodInfo) to contain the DataSourceAttribute.
// Setup mocks
_testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetCustomAttributes(_methodInfo)).Returns(attributes);
_testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetAttributes<DataSourceAttribute>(_methodInfo)).Returns([dataSourceAttribute]);
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodRunnerTests.cs:338
- This GetCustomAttributesCached setup is on the Moq mock, but the code under test calls ReflectionOperations.GetCustomAttributesCached via MockableReflectionOperations, which does not delegate to the mock's GetCustomAttributesCached. The setup is therefore dead and can be removed to avoid confusion.
// Setup mocks
_testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetCustomAttributes(_methodInfo)).Returns(attributes);
_testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetCustomAttributesCached(_methodInfo)).Returns(attributes);
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodRunnerTests.cs:365
- Same issue here: MockReflectionOperations.GetCustomAttributesCached(...) is never invoked when using MockableReflectionOperations, so this setup is unused. Prefer configuring GetCustomAttributes(_methodInfo) instead (which the wrapper uses) or update the wrapper to respect cached setups.
// Setup mocks
_testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetCustomAttributes(_methodInfo)).Returns(attributes);
_testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetCustomAttributesCached(_methodInfo)).Returns(attributes);
- Files reviewed: 26/26 changed files
- Comments generated: 6
| IEnumerable<RetryBaseAttribute> attributes = PlatformServiceProvider.Instance.ReflectionOperations.GetAttributes<RetryBaseAttribute>(MethodInfo); | ||
| using IEnumerator<RetryBaseAttribute> enumerator = attributes.GetEnumerator(); | ||
| if (!enumerator.MoveNext()) | ||
| { | ||
| if (attribute is RetryBaseAttribute retryAttribute) | ||
| { | ||
| if (found is not null) | ||
| { | ||
| ThrowMultipleAttributesException(nameof(RetryBaseAttribute)); | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| found = retryAttribute; | ||
| } | ||
| RetryBaseAttribute attribute = enumerator.Current; | ||
|
|
||
| if (enumerator.MoveNext()) | ||
| { | ||
| ThrowMultipleAttributesException(nameof(RetryBaseAttribute)); | ||
| } | ||
|
|
||
| return found; | ||
| return attribute; |
|
|
||
| // Setup mocks | ||
| _testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetCustomAttributes(_methodInfo)).Returns(attributes); | ||
| _testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetAttributes<DataSourceAttribute>(_methodInfo)).Returns([dataSourceAttribute]); |
|
|
||
| // Setup mocks | ||
| _testablePlatformServiceProvider.MockReflectionOperations.Setup(ro => ro.GetCustomAttributes(_methodInfo)).Returns(attributes); | ||
| _testablePlatformServiceProvider.MockReflectionOperations.Setup(ro => ro.GetCustomAttributesCached(_methodInfo)).Returns(attributes); |
| // PERF: This was moved from Dictionary<MemberInfo, Dictionary<string, object>> to Concurrent<ICustomAttributeProvider, Attribute[]> | ||
| // storing an array allows us to store multiple attributes of the same type if we find them. It also has lower memory footprint, and is faster | ||
| // when we are going through the whole collection. Giving us overall better perf. | ||
| private readonly ConcurrentDictionary<ICustomAttributeProvider, Attribute[]> _attributeCache = []; |
| /// </returns> | ||
| [SecurityCritical] | ||
| #if NET5_0_OR_GREATER | ||
| [Obsolete] |
| internal sealed class ReflectionOperations : MarshalByRefObject, IReflectionOperations | ||
| { |
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Test Expert Reviewer 🧪
Date: 2026-05-11
Repository: microsoft/testfx
Key Findings
-
Dead mock setups in
TestMethodRunnerTests— Six new mock setups (GetAttributes<DataSourceAttribute>×3,GetCustomAttributesCached×3) are never actually called.MockableReflectionOperationsroutes both methods throughmock.GetCustomAttributes, not through the respective generic/cached mock methods. The tests pass correctly via the existingGetCustomAttributessetup, but the extra setups are misleading and create maintenance confusion. -
MockableReflectionOperations.GetCustomAttributesCacheddoesn't cache — While this is an acceptable tradeoff for a test double, the name implies once-per-member semantics. Without a comment documenting this intentional difference, future readers may assume the wrapper has caching behavior equivalent to the productionReflectionOperations. -
Missing coverage for
GetSingleAttributeOrDefaultandGetFirstAttributeOrDefault— Both newIReflectionOperationsmethods lack unit tests. In particular,GetSingleAttributeOrDefault'sInvalidOperationExceptionthrow path andGetFirstAttributeOrDefault's sealed-type assertion are untested.
Positive Observations
- The migration from
SequenceEqual(...).Should().BeTrue()toactual.Should().Equal(expected)throughout the test suite is a meaningful improvement in assertion quality. - The
MockableReflectionOperationsdesign cleanly solves the Moq/generic-method-with-constraints limitation while keeping test setup intuitive. - The
IsAttributeDefinedShouldReturnFromCache/HasAttributeDerivedFromShouldReturnFromCachetests correctly validate caching behavior via thePlatformServiceProvider.Instanceindirection and are architecturally sound. - Integration tests properly migrated from
ReflectionOnlyLoadFromtoAssembly.LoadFrom, removing the fragileReflectionOnlyAssemblyResolvehandler.
Recommendations
- Remove the 6 redundant mock setups in
TestMethodRunnerTests. - Add a clarifying comment to
MockableReflectionOperations.GetCustomAttributesCachedabout the intentional absence of caching. - Add unit tests for
GetSingleAttributeOrDefault(including the multi-attribute exception path) andGetFirstAttributeOrDefault.
Generated by Test Expert Reviewer 🧪
🧪 Test quality reviewed by Test Expert Reviewer 🧪
|
|
||
| // Setup mocks | ||
| _testablePlatformServiceProvider.MockReflectionOperations.Setup(ro => ro.GetCustomAttributes(_methodInfo)).Returns(attributes); | ||
| _testablePlatformServiceProvider.MockReflectionOperations.Setup(ro => ro.GetCustomAttributesCached(_methodInfo)).Returns(attributes); |
There was a problem hiding this comment.
[Structure] This mock setup for GetCustomAttributesCached is also dead code.
MockableReflectionOperations.GetCustomAttributesCached is implemented as:
public Attribute[] GetCustomAttributesCached(ICustomAttributeProvider attributeProvider)
=> attributeProvider switch
{
MemberInfo memberInfo => mock.Object.GetCustomAttributes(memberInfo)?.OfType<Attribute>().ToArray() ?? [],
...
};It calls mock.Object.GetCustomAttributes(...), not mock.Object.GetCustomAttributesCached(...). So this setup is never matched.
Impact: Same as the GetAttributes<T> issue — the GetCustomAttributes setup on the previous line is the one actually driving behavior. The GetCustomAttributesCached setup creates a false sense of test coverage and could mislead future maintainers.
Suggestion: Remove this setup. The same dead-setup pattern appears at lines 337 and 364.
|
|
||
| // Setup mocks | ||
| _testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetCustomAttributes(_methodInfo)).Returns(attributes); | ||
| _testablePlatformServiceProvider.MockReflectionOperations.Setup(rf => rf.GetAttributes<DataSourceAttribute>(_methodInfo)).Returns([dataSourceAttribute]); |
There was a problem hiding this comment.
[Structure] This mock setup for GetAttributes<DataSourceAttribute> is dead code and will never be called.
MockableReflectionOperations.GetAttributes<TAttributeType> is implemented as:
=> GetCustomAttributesCached(attributeProvider).OfType<TAttributeType>();It calls this.GetCustomAttributesCached → mock.Object.GetCustomAttributes(memberInfo). It does not call mock.Object.GetAttributes<T>(...). So this Setup on MockReflectionOperations is never invoked.
Impact: The test appears to control behavior through this setup, but the actual filtering happens from the GetCustomAttributes mock (already set up on the line above). This is misleading to readers and creates maintenance confusion — if someone relies solely on this setup assuming it controls behavior, the test will appear to pass while the actual mock call is something different.
Suggestion: Remove this setup. The GetCustomAttributes setup two lines above is sufficient — MockableReflectionOperations will automatically filter it to DataSourceAttribute instances when GetAttributes<DataSourceAttribute> is called. The same applies to lines 254 and 282.
| where TAttributeType : Attribute | ||
| => GetCustomAttributesCached(attributeProvider).OfType<TAttributeType>(); | ||
|
|
||
| public Attribute[] GetCustomAttributesCached(ICustomAttributeProvider attributeProvider) |
There was a problem hiding this comment.
[Structure] GetCustomAttributesCached on MockableReflectionOperations doesn't actually cache — it re-queries mock.Object.GetCustomAttributes on every call.
The name "Cached" implies once-per-member semantics, but the implementation:
public Attribute[] GetCustomAttributesCached(ICustomAttributeProvider attributeProvider)
=> attributeProvider switch
{
MemberInfo memberInfo => mock.Object.GetCustomAttributes(memberInfo)?.OfType<Attribute>().ToArray() ?? [],
...
};...hits the mock on every invocation.
Impact: Any production-code path that relies on caching to avoid repeated attribute lookups won't be tested correctly when using this test double. A test that calls a method which internally invokes GetCustomAttributesCached ten times would still pass, while production ReflectionOperations would only call the underlying reflection API once. This gap means tests won't catch bugs where caching is accidentally removed.
Suggestion: This is a known tradeoff in a test double. Consider adding a // Note: no caching by design — each call re-queries the mock comment to document the intentional difference from production, so future readers don't assume caching semantics.
| @@ -12,8 +18,33 @@ namespace MSTestAdapter.PlatformServices.UnitTests.Services; | |||
| public class ReflectionOperationsTests : TestContainer | |||
There was a problem hiding this comment.
[Coverage] The new GetSingleAttributeOrDefault<T> and GetFirstAttributeOrDefault<T> methods on ReflectionOperations have no unit tests.
These are non-trivial methods:
GetFirstAttributeOrDefaulthas aDebugEx.Assert(typeof(TAttribute).IsSealed)guard — no test verifies what happens with non-sealed types in debug builds.GetSingleAttributeOrDefaultthrowsInvalidOperationExceptionwhen multiple attributes are found — this error path is entirely untested.
Suggestion: Add at least:
- A happy-path test for each method (attribute found, attribute not found).
- A test that
GetSingleAttributeOrDefaultthrows when two matching attributes are present. - Optionally, a test that
GetFirstAttributeOrDefaultonly returns the first match when multiple are present.
Evangelink
left a comment
There was a problem hiding this comment.
Summary
Workflow: Expert Code Reviewer 🧠
Date: 2026-05-11
Repository: microsoft/testfx
Key Findings
Performance Regression — GetRetryAttribute allocates an iterator state machine (Medium)
TestMethodInfo.GetRetryAttribute() now calls GetAttributes<RetryBaseAttribute>(MethodInfo), which is implemented as a yield return iterator. The old code explicitly avoided this allocation with a comment: "Iterate the cached attribute array directly to avoid allocating the yield-return state machine." Since GetRetryAttribute is called from the TestMethodInfo constructor (once per executed test method), this reintroduces measurable GC pressure at scale. See inline comment.
Defensive Coding Gap — NotCachedReflectionAccessor NullReferenceException + undocumented contract (Low-Medium)
The _ => arm in GetCustomAttributesNotCached evaluates attributeProvider.GetType() in the exception message before guarding against null, so a null input surfaces as NullReferenceException rather than a clean ArgumentNullException. Additionally, valid ICustomAttributeProvider subtypes (ParameterInfo, Module) throw ArgumentException at runtime without the interface contract documenting this constraint. See inline comment.
Positive Observations
- The consolidation of the attribute cache from
ReflectHelper(singleton) intoReflectionOperationsis architecturally sound —ConcurrentDictionary<ICustomAttributeProvider, Attribute[]>is the right data structure for thread-safe, low-memory-footprint attribute caching. - Deletion of
ReflectionOperationsNetFrameworkAttributeHelpersis justified:AssemblyLoadWorkerusesCustomAttributeDatadirectly for reflection-only assemblies, so there are no product code paths that require the old reflection-only-load workaround. GetRetryAttribute's two-step enumerator pattern (get first, check for second) correctly preserves the single-attribute invariant and properly disposes the enumerator viausing.MockableReflectionOperationsis a clean solution to the Moq generic-method-with-constraint limitation.GetTestPropertiesAsTraits's two-pass (count then fill) approach correctly avoids aList<T>allocation for the common zero-traits case.InitializeLifetimeServiceis correctly guarded with#if NET5_0_OR_GREATER [Obsolete]to suppress the BCL's obsolete warning while preserving the infinite-lifetime behavior on .NET Framework.
Recommendations
- Restore the direct cache-array iteration in
GetRetryAttributeto avoid the state-machine allocation. - Add a null guard and a clear exception message in
NotCachedReflectionAccessor.GetCustomAttributesNotCached.
Generated by Expert Code Reviewer 🧠
🧠 Reviewed by Expert Code Reviewer 🧠
| @@ -183,22 +182,20 @@ private TestMethodAttribute GetTestMethodAttribute() | |||
| /// </returns> | |||
| private RetryBaseAttribute? GetRetryAttribute() | |||
| { | |||
There was a problem hiding this comment.
[Performance] This reverses a deliberate per-test-method allocation optimization.
The old GetRetryAttribute had an explicit comment: "Iterate the cached attribute array directly to avoid allocating the yield-return state machine that GetAttributes<T>() would create." The new code calls GetAttributes<RetryBaseAttribute>(MethodInfo), which is backed by a yield return iterator in ReflectionOperations, so it allocates an IEnumerator<RetryBaseAttribute> state-machine object on every call. GetRetryAttribute is called from TestMethodInfo's constructor — i.e., once per test method during execution.
Impact: Small but measurable extra GC pressure proportional to the number of tests. The old optimization path was intentional.
Suggestion: Either iterate the cached array directly (as the old code did) or add an overload to GetCustomAttributesCached / GetAttributes that avoids the state machine — e.g., accepting a callback/delegate pattern or providing a Span<T>-returning variant. The simplest fix consistent with the refactoring goals:
// Iterate directly from the cache, avoiding iterator state machine allocation.
RetryBaseAttribute? found = null;
foreach (Attribute attribute in PlatformServiceProvider.Instance.ReflectionOperations.GetCustomAttributesCached(MethodInfo))
{
if (attribute is RetryBaseAttribute retryAttribute)
{
if (found is not null) ThrowMultipleAttributesException(nameof(RetryBaseAttribute));
found = retryAttribute;
}
}
return found;| { | ||
| IReflectionOperations reflectionOperations = PlatformServiceProvider.Instance.ReflectionOperations; | ||
| object[] attributesArray = attributeProvider switch | ||
| { |
There was a problem hiding this comment.
[Defensive] Two problems in the _ => default arm of the switch:
-
NullReferenceExceptionon null input: IfattributeProviderisnullit reaches the_arm, but thenattributeProvider.GetType()in the interpolated string throwsNullReferenceExceptionbefore theArgumentExceptionis constructed. The caller seesNREinstead of a meaningfulArgumentNullException. -
Undocumented contract restriction:
IReflectionOperations.GetCustomAttributesCachedacceptsICustomAttributeProvider, which includesParameterInfoandModule— both validICustomAttributeProviderimplementations. Passing either throwsArgumentExceptionat runtime without being documented in the interface XML doc or enforced by the type system.
Impact: Callers of the internal interface who (reasonably) pass a ParameterInfo (e.g., for parameter-level attributes) get an unexpected runtime exception rather than a compile-time error. Null input silently escalates to NRE.
Suggestion:
if (attributeProvider is null)
throw new ArgumentNullException(nameof(attributeProvider));
// ...switch...
_ => throw new ArgumentException(
$"Unsupported attribute provider type: {attributeProvider.GetType()}. " +
"Only MemberInfo and Assembly are supported.", nameof(attributeProvider)),Also consider adding /// <exception cref="ArgumentException">Thrown for unsupported provider types.</exception> to IReflectionOperations.GetCustomAttributesCached.
Summary
Unify the reflection helper infrastructure by consolidating
ReflectHelperlogic intoReflectionOperations(theIReflectionOperationsimplementation). This eliminates a layer of indirection and makes the reflection surface area consistent across the adapter and platform services.Changes
Consolidation into
IReflectionOperations/ReflectionOperationsIReflectionOperationswith higher-level generic methods previously only available onReflectHelper:IsAttributeDefined<TAttribute>GetFirstAttributeOrDefault<TAttribute>GetSingleAttributeOrDefault<TAttribute>GetAttributes<TAttributeType>GetCustomAttributesCachedIsMethodDeclaredInSameAssemblyAsTypeConcurrentDictionary<ICustomAttributeProvider, Attribute[]>) fromReflectHelperintoReflectionOperations.ReflectionOperationsinherit fromMarshalByRefObject(needed for .NET Framework app-domain isolation).New helper:
ReflectionHelper(extension methods)ReflectionHelperinMSTestAdapter.PlatformServiceswith extension methods onIReflectionOperationsfor:GetTestCategories— collectsTestCategoryBaseAttributefrom method, type, and assembly levels.GetTestPropertiesAsTraits— extractsTestPropertyAttributevalues asTrait[].Removed code
ReflectHelpersingleton and its per-file usages across the adapter and platform services.ReflectionOperationsNetFrameworkAttributeHelpers.cs— the .NET Framework-specific reflection-only-load logic is no longer needed.ReflectHelperTestsunit test file (tests moved toReflectionOperationsTests).Call-site migrations
ReflectHelper.Instanceto go throughPlatformServiceProvider.Instance.ReflectionOperationsinstead:TestAssemblySettingsProvider,TestMethodInfo,TestMethodRunner,AttributeHelpers,TestDeployment,DeploymentItemUtility,DeploymentUtilityBase.Test infrastructure
MockableReflectionOperations— a wrapper that delegates non-genericIReflectionOperationsmethods to a Moq mock, and implements the new generic methods by filtering mock results. This solves the problem of Moq not supporting generic methods with type constraints.TestablePlatformServiceProvider.SetupMockReflectionOperations()to use the new wrapper.ReflectHelperTestsintoReflectionOperationsTests(attribute caching,IsAttributeDefined,GetTestCategories, etc.).ReflectionUtilityTests(integration tests) to useAssembly.LoadFrominstead of the removedAssembly.ReflectionOnlyLoadFrom.GetAttributes<T>generic method on both unit and integration test levels.